一路做到 Day 28,目前我們功能其實已經相當完整。
但專案做到這個階段後,最重要的事情反而不是繼續加功能,而是:
把所有功能一起操作找出彼此衝突的地方並且修正 Bug
還有整理程式確認 Desktop / Mobile 都能正常操作
所以 Day 29 的主題就是:Integration + Debug
Day 29 不加入大型新功能,而是把前面做過的功能全部重新測試與修正。
Day 30 就會正式進入最終完整作品。
今天主要處理:
| 問題 | Day 29 修正 |
|---|---|
| Focus Camera | 不再每幀強制重設 Camera Offset |
| OrbitControls | Focus 時仍可自由旋轉 |
| Planet Selection | Pointer 移動超過一定距離就不算 Click |
| Mobile Touch | 避免滑動畫面時誤選 Planet |
| UI | 小螢幕控制面板可捲動 |
| Debug | 建立完整功能測試流程 |
Day 27 我們使用:
camera.position.copy(
selectedPlanet.mesh.position
);
camera.position.add(
focusCameraOffset
);
每一幀讓 Camera 跟著 Planet 的這個概念沒有錯。
但實際操作後會發現:
進入 Planet Focus 使用滑鼠旋轉 Camera
而下一幀 Camera 又被原本的 focusCameraOffset 拉回去
原因就是我們每一幀都重新指定 Camera Position
所以 OrbitControls 剛剛做出的 Camera 旋轉結果,很容易被覆蓋。
我們真正需要的是:
Planet 往前移動多少 → Camera 跟著移動多少
而不是:
每一幀 → 重新設定 Camera 固定位置
所以今天改成記錄:
const previousFocusPosition =
new THREE.Vector3();
它負責記錄:
上一幀 Planet 在哪裡
修改 javascript:
function focusPlanet(
planet
) {
selectedPlanet =
planet;
focusMode =
true;
const focusDistance =
planet.size *
6 +
3;
camera.position.set(
planet.mesh.position.x +
focusDistance,
planet.mesh.position.y +
focusDistance *
0.5,
planet.mesh.position.z +
focusDistance
);
controls.target.copy(
planet.mesh.position
);
previousFocusPosition.copy(
planet.mesh.position
);
controls.update();
showPlanetInfo(
planet
);
}
注意今天已經不需要原本的 focusCameraOffset 了。
建立:
const focusMovement =
new THREE.Vector3();
Animation 裡:
if (
focusMode &&
selectedPlanet
) {
focusMovement
.copy(
selectedPlanet
.mesh
.position
)
.sub(
previousFocusPosition
);
}
代表現在位置 - 上一幀位置 = 這一幀移動多少
例如:
上一幀 Earth
X = 5.00
Z = 3.00
這一幀 Earth
X = 5.05
Z = 3.02
那 Movement
X = 0.05
Z = 0.02
取得 Movement 後:
camera.position.add(
focusMovement
);
也就是 Planet 往右移動 0.05
Camera 也往右移動 0.05
接著:
controls.target.copy(
selectedPlanet.mesh.position
);
Camera 仍然以 Planet 為中心。
最後更新:
previousFocusPosition.copy(
selectedPlanet.mesh.position
);
以前:
Planet 移動 Camera 被強制放回固定 Offset
OrbitControls 操作容易被覆蓋
現在:
Planet 移動計算 Planet Movement
Camera 只跟著平移 OrbitControls 自己控制旋轉角度
因此進入 Earth Focus 後仍然可以
拖曳滑鼠繞 Earth 觀看 + 滾輪可以靠近 / 遠離 Earth
同時 Earth 繼續公轉,
這才比較符合我們想要的 Planet Tracking + OrbitControls。
Day 26 我們使用:
renderer.domElement.addEventListener(
"pointerdown",
...
);
問題是 Pointer Down
不一定代表使用者真的想選 Planet
他可能只是想按住滑鼠拖動畫面
如果剛好按在 Planet 上
Camera 還沒開始轉 Planet 就被選中了
在手機上也會發生類似情況:
所以今天要把 Click 與 Drag 區分開。
新增:
const pointerStart =
new THREE.Vector2();
按下滑鼠時:
renderer.domElement.addEventListener(
"pointerdown",
(event) => {
pointerStart.set(
event.clientX,
event.clientY
);
}
);
只是單純記住滑鼠從哪裡開始
放開時:
const pointerEnd =
new THREE.Vector2(
event.clientX,
event.clientY
);
再計算:
const pointerDistance =
pointerStart.distanceTo(
pointerEnd
);
十二、移動太多就當作 Camera Drag
設定:
if (
pointerDistance >
5
) {
return;
}
意思就是:
滑鼠移動 ≤ 5px → 視為 Click
滑鼠移動 > 5px → 視為 Drag 不執行 Raycaster
因此:
點 Planet → Select Planet
拖動畫面經過 Planet → 不 Select
操作會自然很多。
Day 29 我們順便把 Planet Selection 整理一下。
建立:
function selectPlanet(
event
) {
const rect =
renderer.domElement
.getBoundingClientRect();
mouse.x =
(
(
event.clientX -
rect.left
) /
rect.width
) *
2 -
1;
mouse.y =
-(
(
event.clientY -
rect.top
) /
rect.height
) *
2 +
1;
raycaster.setFromCamera(
mouse,
camera
);
const planetMeshes =
planets.map(
(planet) =>
planet.mesh
);
const intersects =
raycaster.intersectObjects(
planetMeshes,
false
);
if (
intersects.length ===
0
) {
return;
}
const clickedMesh =
intersects[0].object;
const clickedPlanet =
planets.find(
(planet) =>
planet.mesh ===
clickedMesh
);
if (
!clickedPlanet
) {
return;
}
resetOrbitColors();
highlightOrbit(
clickedPlanet.name
);
focusPlanet(
clickedPlanet
);
}
現在:
renderer.domElement.addEventListener(
"pointerup",
(event) => {
const pointerEnd =
new THREE.Vector2(
event.clientX,
event.clientY
);
const pointerDistance =
pointerStart.distanceTo(
pointerEnd
);
if (
pointerDistance >
5
) {
return;
}
selectPlanet(
event
);
}
);
流程變成:
Pointer Down → 記住開始位置
Pointer Move → OrbitControls 可以正常處理
Pointer Up → 計算移動距離是否小於 5px?
→ IF YES → Raycaster → Planet Selection
這是今天很重要的整合測試。
假設現在 Focus Earth
再按 Pause 此時:
Earth → 停止公轉
Moon → 停止運動
Sun → 停止旋轉
但 OrbitControls 仍然在 Animation Loop 裡更新。
所以仍可以繞著 Earth 旋轉 Camera
這就是我們前面刻意沒有停止:
requestAnimationFrame(
animate
);
的原因。
我們停止的是天體動畫更新而不是整個 Render Loop
接著測 Earth Focus Speed 5x
Earth 會快速公轉。
現在 Focus Mode 使用的是 Planet Movement
所以 Camera 也會跟著 Earth 的移動量一起移動。
因此理論上任何速度都可以正常追蹤。
這就是整合 Debug 很重要的地方。
單獨看 Speed 沒有問題。
單獨看 Focus 也沒有問題。
但最後一定要測 Speed + Focus 一起執行。
還需要測點 Earth
Earth Orbit 黃色
關閉「顯示軌道」
因為我們是修改 orbit.line.visible
所以 Earth Orbit 雖然還保留黃色 Material,但會被隱藏。
重新開啟顯示軌道 Earth Orbit 還是黃色,這其實是合理的
因為 selectedPlanet 仍然是 Earth。
點返回太陽系應該同時完成:
focusMode = false
selectedPlanet = null
Camera 回到初始位置
controls.target 回 Sun
Planet Info 消失
Orbit Highlight 清除
目前:
function returnToSolarSystem() {
focusMode =
false;
selectedPlanet =
null;
if (
window.innerWidth <=
768
) {
camera.position.set(
45,
40,
55
);
} else {
camera.position.set(
30,
25,
30
);
}
controls.target.set(
0,
0,
0
);
controls.update();
planetInfo.classList.remove(
"active"
);
resetOrbitColors();
}
Desktop 與 Mobile 也會分別回到不同距離。
我們還有四種 Camera View。
所以一定要測Focus Saturn 時按 3
這時應該:
退出 Focus
↓
Planet Info 消失
↓
Orbit 恢復灰色
↓
切換到 Camera View 3
因此 Keyboard Event 裡保留:
focusMode =
false;
selectedPlanet =
null;
planetInfo.classList.remove(
"active"
);
resetOrbitColors();
手機高度比較短時 Control Panel 有可能超過畫面高度。
所以今天加入:
@media (
max-width: 768px
) {
.control-panel {
max-height:
calc(
100vh -
110px
);
overflow-y:
auto;
}
}
如果控制面板超出手機畫面
不會直接被裁掉可以上下捲動
今天完成 Pointer 修正後:
滑動 → OrbitControls
Tap Planet → Raycaster
Pinch → Zoom
在手機上會比直接使用 pointerdown 選 Planet 更容易操作。
這也是為什麼我們使用 Pointer Events 而不是只處理 mousedown
因為 Pointer Event 可以比較統一處理:
Mouse
Touch
Pen
今天文章很適合讓讀者真正把功能逐項測一次。
先啟動 Live Server。
然後按照下面順序操作:
如果這些都正常代表核心功能整合完成
Day 28 已經提過手機測試,今天 Debug 時一定要真的再測一次。
首先手機 + 電腦連同一個 Wi-Fi。
Windows CMD:ipconfig
例如找到:
IPv4 Address 192.168.1.105
Live Server 如果是:
http://127.0.0.1:5500/
手機輸入:
http://192.168.1.105:5500/
接著實際測:
單指拖曳
雙指縮放
點擊 Planet
Focus Planet
Play / Pause
Slider
返回太陽系
因為 Chrome Device Mode 比較適合檢查 Responsive Layout
真正手機則可以檢查 Touch + Performance + 操作手感 兩種最好都測。
今天的重點不是又多做了一個功能
而是第一次真正以「使用者會怎麼操作?」來檢查整個作品。
我們修正了 Planet Focus + OrbitControls 互相衝突的問題。
以前 Camera 每幀都被強制放回固定 Offset。
現在改成 Planet Movement 時 Camera 可以跟著平移
因此使用者可以 Focus Planet + 自由旋轉 Camera 同時存在。
另外我們也把 Pointer Down
改成判斷:
Click
or
Drag
避免想旋轉畫面卻誤選 Planet。
最後今天最重要的工作就是把所有功能全部操作一次
如果你是讀者,也不要只確認畫面有出現
而是實際測每個功能是不是都正常
因為一個功能單獨正常不代表整合之後也一定正常
這就是 Debug 階段真正重要的地方。
「Final Project:30 天打造互動式太陽系」
終於剩最後一天,Day 30 我們會把 30 天的內容全部整合。
會整理完整:
solar-system/
│
├── index.html
│
├── css/
│ └── style.css
│
├── js/
│ └── main.js
│
└── textures/
├── sun.jpg
├── mercury.jpg
├── venus.jpg
├── venus_atmosphere.jpg
├── earth.jpg
├── earth_night.jpg
├── moon.jpg
├── mars.jpg
├── jupiter.jpg
├── saturn.jpg
├── saturn_ring.png
├── uranus.jpg
└── neptune.jpg
並提供
完整 index.html
完整 style.css
完整 main.js
Texture 檔案說明
專案架構
功能整理
作品說明
30 天總結
Day 30 就會是這個系列真正的 Final Version